package obecto.transition
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	
	import obecto.event.TaskEvent;
	import obecto.utility.Listener;
	
	public class TransitionsManager extends EventDispatcher implements ITransitionsResolver
	{
		[ArrayElementType("obecto.transition.State")]
		[Bindable] 
		public var transitionStates : Array;
		
		[ArrayElementType("obecto.transition.Transition")]
		[Bindable] 
		public var transitions : Array;
		
		public var transitionsResolver : ITransitionsResolver;
		
		public var interruptionPolicy : TransitionInterruptionPolicy = TransitionInterruptionPolicy.INTERRUPT;
		
		public var autoReverse : Boolean = false;
		
		/**
		 * Represents the initial State of the TransitionManager.
		 * This property should always be initialized prior to requesting a new state!
		 */ 
		public function set initialState(value : Object) : void 
		{
			internalCurrentState = resolveState(value);
			
			// the current state should be activated, so it will dispatch enter and leave events:
			currentState.enter();
		}
		
		public function set state(value : Object) : void
		{
			if (value == null) return;

			if ((value is State && requestedState != value) || 
				(value is String && (requestedState == null || requestedState.name != value)))
			{
				requestedState = resolveState(value);
				transit();
			}
		}
		
		/**
		 * Represents the currently applied state or the last known state of the component.
		 */ 
		[Bindable("currentStateChange")]
		public function get currentState() : State
		{
			return _currentState;
		}
		
		/**
		 * Represents the requested state which may or may not be applied depending on 
		 * wheather the transition is interrupted.
		 * In most of the cases pendingState and targetState would be the same.
		 */ 
		[Bindable("pendingStateChange")]
		public function get pendingState() : State
		{
			return _pendingState;
		}
		
		/**
		 * Represents the state to which we're transiting. 
		 * In most of the cases targetState and pendingState would be the same.
		 * In case of interruption targetState is the new state and pendingState
		 * is the previous targetState.
		 */ 
		[Bindable("targetStateChange")]
		public function get targetState() : State
		{
			return _targetState;
		}
		
		/**
		 * The transition which is currently played.
		 */ 
		[Bindable("currentTransitionChange")]
		public function get currentTransition() : ITransition
		{
			return _currentTransition;
		}
		
		[Bindable("transitingChange")]
		public function get transiting() : Boolean
		{
			return _transiting;
		}
		
		protected var _currentState : State;
		protected function set internalCurrentState(value : State) : void
		{
			if (value != _currentState)
			{
				_currentState = value;
				dispatchEvent(new Event("currentStateChange"));
			}
		}
		
		
		protected var _pendingState : State;
		protected function set internalPendingState(value : State) : void
		{
			if (value != _pendingState)
			{
				_pendingState = value;
				dispatchEvent(new Event("pendingStateChange"));
			}
		}
		
		protected var _targetState : State;
		protected function set internalTargetState(value : State) : void
		{
			if (value != _targetState)
			{
				_targetState = value;
				dispatchEvent(new Event("targetStateChange"));
			}
		}
		
		protected var _currentTransition : ITransition;
		protected function set internalCurrentTransition(value : ITransition) : void
		{
			if (value != _currentTransition)
			{
				_currentTransition = value;
				dispatchEvent(new Event("currentTransitionChange"));
			}
		}
		
		protected var _transiting : Boolean;
		protected function set internalTransiting(value : Boolean) : void
		{
			if (value != _transiting)
			{
				_transiting = value;
				dispatchEvent(new Event("transitingChange"));
			}
		}
		
		protected function get autoReverseApplicable() : Boolean
		{
			return (autoReverse || currentTransition.autoReverse) && 
				(currentTransition.transitingFrom == targetState && currentTransition.transitingTo == pendingState);
		}
		
		protected function get undefinedState() : State
		{
			var result : State = new State();
			result.name = "undefined";
			return result;
		}
		
		protected var requestedState : State;
		protected var transitionCompleteListener : Listener;
		
		public function TransitionsManager(target : IEventDispatcher = null)
		{
			super(target);
			
			// by default the transition manager is its transitions resolver:
			transitionsResolver = this;
		}
		
		public function resolveState(value : Object) : State
		{
			if (value is State)
			{
				if (transitionStates.indexOf(value) != -1)
				{
					return State(value);
				}
			}
			
			if (value is String)
			{
				for each (var st : State in transitionStates)
				{
					if (st.name == value)
					{
						return st;
					}
				}
			}
			
			return null;
		}
		
		public function resolveTransition(fromState : State, toState : State) : ITransition
		{
			var matches : Array = [];
			
			// find matching transitions;
			for each (var transition : ITransition in transitions)
			{
				if (isMatch(transition, fromState, toState))
				{
					matches.push(transition);
				}
			}
			
			var exactMatches : Array = [];
			if (matches.length > 1)
			{
				// in case of multiple matching transitions leave only the exact matches:
				for each (var match : ITransition in transition)
				{
					if (isExactMatch(match))
					{
						exactMatches.push(match);
					}
				}
			}
			else
			{
				exactMatches = matches;
			}
			
			if (exactMatches.length > 1)
			{
				throw new Error("There is more than one transition entry for A->B or *->B");
			}
			
			// finding no matching transitions should not result in a runtime exception, so just returning null:
			return exactMatches.length !=0 ? exactMatches[0] : null;
		}
		
		public function transitImmediately(toState : State) : void 
		{
			if (transitionCompleteListener != null)
			{
				transitionCompleteListener.stop();
				transitionCompleteListener = null;
			}
			
			if (transiting)
			{
				currentTransition.interrupt();
			}
			
			internalTransiting = false;
			internalTargetState = null;
			internalPendingState = null;
			
			currentState.leave();
			internalCurrentState = requestedState = toState;
			currentState.enter();
		}

		protected function transit() : void 
		{
			updatePendingAndTargetStates();
			
			if (transiting)
			{
				interruptCurrentTransition();
			}
			else
			{
				var transition : ITransition = internalResolveTransition(currentState, targetState);
				executeTransition(transition);
			}
		}
		
		protected function updatePendingAndTargetStates() : void
		{
			if (transiting)
			{
				internalPendingState = targetState;
				internalTargetState = requestedState;
			}
			else
			{
				internalPendingState = internalTargetState = requestedState;
			}
		} 
		
		protected function interruptCurrentTransition() : void 
		{
			if (autoReverseApplicable)
			{
				reverse();
				return;
			}
			
			var interruptionPolicy : TransitionInterruptionPolicy = determineInterruptionPolicy();
			switch (interruptionPolicy)
			{
				case TransitionInterruptionPolicy.END: // synchronous
					endTransition();
				break;
				
				case TransitionInterruptionPolicy.ROLLBACK: // synchronous
					rollbackTransition();
				break;
				
				case TransitionInterruptionPolicy.INTERRUPT: // synchronous
					interruptTransition();
				break;
				
				case TransitionInterruptionPolicy.REVERSE: // asynchronous - wait until reverse completes
					reverseTransition();
				break;
				
				case TransitionInterruptionPolicy.WAIT: // asynchronous - wait until completed
					waitTransition();
				break;
			}
		}
		
		protected function reverse() : void 
		{
			if (currentTransition.reversing)
			{
				currentTransition.forward();
			}
			else
			{
				currentTransition.reverse();
			}
		}
		
		protected function determineInterruptionPolicy() : TransitionInterruptionPolicy
		{
			if (currentTransition.interruptionPolicy)
			{
				return currentTransition.interruptionPolicy;
			}
				
			return interruptionPolicy;
		}
		
		protected function endTransition() : void 
		{
			// end should dispatch TaskEvent.COMPLETE, so this should be enough:
			currentTransition.end();
		}
		
		protected function rollbackTransition() : void
		{
			// rollback should dispatch TaskEvent.COMPLETE, so this should be enough:
			currentTransition.rollback();
		}
		
		protected function interruptTransition() : void
		{
			/*trace("transition INTERRUPT: " + 
				(currentTransition && currentTransition.transitingFrom ? currentTransition.transitingFrom.name : null) + "-->" + 
				(currentTransition && currentTransition.transitingTo ? currentTransition.transitingTo.name : null) + 
				" | currentState: " + (currentState ? currentState.name : null) + 
				" | pendingState: " + (pendingState ? pendingState.name : null) + 
				" | targetState: " + (targetState ? targetState.name : null));*/
			
			// interrupt does nothing but interrupting the transition and dispatching TaskEvent.INTERRUPT:
			currentTransition.interrupt();
			
			// since transition is interrupted you need to clean the listener, so it won't execute if 
			// the same transition is played again later:
			transitionCompleteListener.stop();
			transitionCompleteListener = null;
			
			// the state left after such interrupt is undefined, so resolveTransition will look for (* --> TARGET):
			internalCurrentState = undefinedState;
			
			// the transition is no longer executing:
			internalTransiting = false;
			
			// start the next transition:
			var transition : ITransition = internalResolveTransition(currentState, targetState);
			executeTransition(transition);
		}
		
		protected function reverseTransition() : void
		{
			// here you can reverse only a transition that hasn't been reversed yet:
			if (!currentTransition.reversing)
			{
				// when reverse completes it will dispatch TaskEvent.COMPLETE, so this should be enough:
				currentTransition.reverse();
			}
		}
		
		protected function waitTransition() : void 
		{
			// do nothing here, the current transition must be waited for completion.
		}
		
		protected function internalResolveTransition(fromState : State, toState : State) : ITransition
		{
			return transitionsResolver.resolveTransition(fromState, toState);
		} 
		
		protected function executeTransition(transition : ITransition) : void 
		{	
			if (transition != null)
			{
				transition.transitingFrom = currentState;
				transition.transitingTo = targetState;
				
				internalCurrentTransition = transition;
				internalTransiting = true;
				
				/*trace("transition EXECUTE: " + 
					(currentTransition && currentTransition.transitingFrom ? currentTransition.transitingFrom.name : null) + "-->" + 
					(currentTransition && currentTransition.transitingTo ? currentTransition.transitingTo.name : null) + 
					" | currentState: " + (currentState ? currentState.name : null) + 
					" | pendingState: " + (pendingState ? pendingState.name : null) + 
					" | targetState: " + (targetState ? targetState.name : null));*/
				
				transitionCompleteListener = Listener.createSingleTimeListener(currentTransition, TaskEvent.COMPLETE, handleTransitionComplete);
					
				currentState.leave();
						
				transition.start();
			}
			else // change states without starting transition:
			{
				currentState.leave();
				internalCurrentState = pendingState;
				currentState.enter();
				
				internalTargetState = null;
				internalPendingState = null;
			}
		}
		
		
		
		protected function isMatch(transition : ITransition, fromState : State, toState : State) : Boolean
		{
			var fromStatesArray : Array = ([]).concat(transition.fromState);
			var toStatesArray : Array = ([]).concat(transition.toState);
			
			var item : Object;
			var isMatch : Boolean = false;
			
			for each (item in fromStatesArray)
			{
				isMatch = item == fromState || item == fromState.name || item == "*";
				if (isMatch) break;
			}
			if (!isMatch) return false;
			
			for each (item in toStatesArray)
			{
				isMatch = item == toState || item == toState.name || item == "*";
				if (isMatch) break;
			}
			return isMatch;
		}
		
		protected function isExactMatch(transition : ITransition): Boolean
		{
			var fromStatesArray : Array = ([]).concat(transition.fromState);
			var toStatesArray : Array = ([]).concat(transition.toState);
			
			fromStatesArray.concat(transition.fromState);
			toStatesArray.concat(transition.toState);
			
			return !(fromStatesArray.length == 1 && fromStatesArray[0] == "*") && 
				!(toStatesArray.length == 1 && toStatesArray[0] == "*");
		}
		
		protected function handleTransitionComplete(e : TaskEvent) : void
		{
			/*trace("transition COMPLETE: " + 
				(currentTransition && currentTransition.transitingFrom ? currentTransition.transitingFrom.name : null) + "-->" + 
				(currentTransition && currentTransition.transitingTo ? currentTransition.transitingTo.name : null) + 
				" | currentState: " + (currentState ? currentState.name : null) + 
				" | pendingState: " + (pendingState ? pendingState.name : null) + 
				" | targetState: " + (targetState ? targetState.name : null));*/
			
			transitionCompleteListener = null;
			
			internalTransiting = false;
			
			internalCurrentTransition = null;
			
			internalCurrentState = pendingState;
			currentState.enter();
			
			if (pendingState == targetState)
			{
				internalTargetState = null;
				internalPendingState = null;
			}
			else // there is a pending request for a new state, so another transition should start immediately:
			{
				requestedState = targetState;
				transit();
			}
		}
		
	}
}